233

     # UI | 11 | Playwright: Page Object паттерн


Введение

По сути то, что мы сделали в прошлых главах - в какой-то мере является отображением логики Page Object, а именно:

  • локаторы хранились отдельно
  • тесты отдельно

то есть, мы попытались разделить логику автотестов и "реализацию".

Паттерн Page Object подразумевает использование классов, поэтому для реализации потребуются более глубокие познания в Python и ООП.

Смысл этого паттерна состоит в разделении задач:

  • в отдельном классе хранится взаимодействие со страницей
  • отдельный класс описывает работу с логикой web-элементов
  • тесты хранятся в самостоятельных py-модулях

Но ключевое - это то, что хранение локаторов осуществляется на уровне страниц. Другими словами, для каждой тестируемой страницы создаётся свой объект, который описывает, какие элементы имеются на этой странице.


Наш проект будет иметь следующую структуру:

pages
  base.py # взаимодействие с браузером
  elements.py # логика работы с web-элементами
  xxx.py # любые объекты наших страниц
tests
  test_1.py # первый тест
  xxx.py # любое кол-во тестов
run_po.py # запуск тестирования

Начинаем постепенно переносить логику:


base.py - работа с браузером

Финальная цель (помимо описанного в прошлой главе) - разнести логику "по разным углам":

  • за взаимодействие с браузером отвечает один класс
  • за локаторы другой
  • за действия над элементами третий
  • за тестирование четвёртый

Ниже создаём класс, который будет отвечать за работу с браузером, а также создадим ещё один скрипт, через который будет запускать само тестирование:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# base.py

from playwright.sync_api import sync_playwright, expect

class Test_browser():
    def __init__(self):
        self.playwright = sync_playwright().start()
        self.browser = self.playwright.chromium.launch(headless=False)
        self.page = self.browser.new_page()

    def close_browser(self):
        self.browser.close()
        self.playwright.stop()

    def open_url(self, url):
        self.page.goto(url)


# run_po.py

import time

from pages.base import Test_browser

pw_browser = Test_browser()
time.sleep(5)
pw_browser.close_browser()

Убеждаемся, что ничего не поломалось, браузер открывается и через 5 секунд закрывается: python3 run_po.py.

Самостотельное храненение локаторов

Далее переносим все наши локаторы. Они создавались для страницы g-oak.ru, то есть для главной страницы. Так и назовём этот класс => main_page.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# main_page.py

class Main_page():
    def __init__(self, page):
        self.page = page

        # Test 1
        self.top_menu_children = self.page.locator("#ul_mainmenu")
        self.top_menu_options = self.page.locator("#ul_mainmenu li")
        self.top_menu_options_as_list = self.top_menu_options.all()
        self.top_menu_articles = self.page.locator("#ul_mainmenu div").get_by_role("img")
        self.top_menu_articles_chapters = self.page.locator('//btn[text()="# > "]')

        # Test 2
        self.body_title = self.page.get_by_role("heading", name="Что новенького")
        self.body_all_articles = self.page.locator('//li[@class="article_entry"]')
        self.body_all_articles_btn_open = self.page.locator('//label[text()="Читать полностью"]')

        # Test 3
        self.body_all_articles_title_list = self.page.locator("//label[@id='index_title']").all()
        self.body_all_articles_description_list = self.page.locator("//p[@class='annonce']").all()
        self.body_all_articles_date_list = self.page.locator("//div[2]/label").all()
        self.body_all_articles_view_list = self.page.locator('//strong[text()="Просмотры"]/../following-sibling::*').all()
        self.body_all_articles_like_list = self.page.locator('//strong[text()="Лайки"]/../following-sibling::*').all()

        # Tests 4, 5
        self.footer_btn_children = self.page.locator('//*[@id="index_footer"]//td//strong')
        self.footer_btn_prev_page = self.page.locator("#previous_page")
        self.footer_btn_prev_pages = self.page.locator("#previous_pages")
        self.footer_btn_next_page = self.page.locator("#next_page")
        self.footer_btn_pagination_field = self.page.locator("#current_page")
        self.footer_btn_pagination_field_max_value = int(self.page.locator('//label[@id="max_page"]').inner_text().replace("/", "").strip())

Мы будем обращаться к элементам этого класса из других модулей, поэтому они сохранены через self. Иначе у нас такой возможности не будет.


Подготовка первого теста и запуск

Теперь - когда есть элементы, создаём:

  • файл, отвечающий за проверку элементов
  • пишем первый тест-модуль
pages
  base.py # взаимодействие с браузером
  elements.py # требуется создать
  main_page.py # локаторы главной страницы
tests
  test_1.py # требуется создать
  xxx.py # любое кол-во тестов
run_po.py # запуск тестирования

Внутри test_1.py также сделаем логику более очевидной за счёт использования функций.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# test_1.py

from playwright.sync_api import expect

from pages.base import Test_browser
from pages.main_page import Main_page

def launch_smoke_1():
    pw_browser = Test_browser()

    def open_page():
        pw_browser.open_url("https://g-oak.ru")

    def finish_testing():
        pw_browser.close_browser()

    def do_smoke_test():
        print(f"\nТест 1: старт")
        main_page = Main_page(pw_browser.page)

        print("[DO] Проверка главного меню")
        expect(main_page.top_menu_children).to_be_attached(timeout=5000)
        print("[OK] Проверка главного меню")

        print("[DO] Проверка элементов главного меню")
        expect(main_page.top_menu_options).to_have_count(4)
        print("[OK] Проверка элементов главного меню")

        print("[DO] Проверка видимости элементов главного меню")
        for elem in main_page.top_menu_options_as_list:
            expect(elem).to_be_visible()
        print("[OK] Проверка видимости элементов главного меню")

    open_page()
    do_smoke_test()
    finish_testing()

Что здесь происходит построчно:

  • from playwright.sync_api import expect - нам пришлось импортировать expect, так как он активно используется в тесте.
  • from pages.base import Test_browser - нам требуется импортировать браузер, так как в рамках теста мы планируем первым делом открыть его для проведения тестирования
  • from pages.main_page import Main_page - в ходе теста мы обращаемся к локаторам, они хранятся в этом классе, поэтому импортируем его
  • def launch_smoke_1(): - оборачиваем весь тест в функцию, так как будем вызывать её отдельно из другого модуля (об этом подробнее позже)
  • pw_browser = Test_browser() - создаём объект браузера, то есть, открываем отдельный брузер через Playwright для проведения тестирования. Если требуется поменять "настройки открытия" - это делается в файле base.py.
  • open_page, do_smoke_test, finish_testing - три функции, отвечающие за логику этого теста
  • open_page() - отвечает за открытие новой страницы, ранее мы запускали только браузер, но не открывали никакой url.
  • do_smoke_test() - запуск теста
  • main_page = Main_page(pw_browser.page) - создаём экземпляр страницы main_page, это требуется, чтобы обращаться к локаторам, которые сохранены в этой странице.

Далее все проверки выглядят одинаково и уже описывались ранее. Единственное отличие состоит в том, что мы обращаемся ко всем локаторам через экзмепляр класса main_page. + имя_локатора:

expect(main_page.top_menu_options).to_have_count(4)

Тем самым, если он однажды изменится, мы просто подредактируем класс этой страницы и все тесты по-прежнему будут работать.


Запуск теста

Как ранее говорилось, сам тест мы планируем запускать через python3 run_po.py, поэтому редактируем этот файл, удаляя всё, что там было и пишем:

from tests.test_1 import launch_smoke_1

launch_smoke_1()

Мы просто говорим - запускай напрямую launch_smoke_1(). Всё, что этому модулю требуется:

  • какой браузер
  • какая страница
  • элементы

Всё описано в нём, он сам всё поднимет и запустит.

python3 run_po.py 

Тест 1: старт
[DO] Проверка главного меню
[OK] Проверка главного меню
[DO] Проверка элементов главного меню
[OK] Проверка элементов главного меню
[DO] Проверка видимости элементов главного меню
[OK] Проверка видимости элементов главного меню

Зачем нужен файл elements.py

Во-первых, все упомянутые выше названия файлов могут отличаться и файлы можно создавать под любыми именами.

Мы же используем эти названия для сохранения наглядности:

  • base.py - для работы с браузером
  • elements.py - логика работы с элементами

Просто так получилось, что в рамках описанного теста мы использовали только expect - это относится к внутренней логике теста. У нас нет какой-то особой логики для работы с элементами, поэтому нам нечего выносить в elements.py.

Проверка загруженности страницы? Нами проверялась только одна - главная - страница и эту логику мы вынесли в base.py, в функцию open_url. При желании можно расширить эту функцию, добавив проверку:

def open_url(self, url):
    self.page.goto(url)
    self.page.wait_for_load_state()

С остальынми элементами:

  • мы, во-первых, работали через expect
  • во-вторых, проверили досутпность "главного"
expect(main_page.top_menu_children).to_be_attached(timeout=5000)

Наверное, нет смысла проверять каждый из них отдельно, раз уж родительский доступен.

Но на самом деле у нас есть логика обхода элементов в виде списка, которая применяется не только в этом тесте:

print("[DO] Проверка видимости элементов главного меню")
for elem in main_page.top_menu_options_as_list:
    expect(elem).to_be_visible()
print("[OK] Проверка видимости элементов главного меню")

Получается, чтобы следовать правилу Don't Repeat Yourself, есть смысл вынести это в виде отдельного метода, напр., в elements.py, так как это поможет нам в будущем сократить количество повторяемого кода:

# elements.py

from playwright.sync_api import expect

class Web_element():
    def __init__(self, locator):
        self.locator = locator

    def check_visibility_of_elements_in_the_list(self):
        print(f"локатор: {self.locator}")
        print(f"тип: {type(self.locator)}")
        assert isinstance(self.locator, list)
        for elem in self.locator:
            print(type(elem))
            assert "Locator" in str(elem)
            expect(elem).to_be_visible()

После чего в нашем тесте для проверки списка используем:

  • не
for elem in main_page.top_menu_options_as_list:
    expect(elem).to_be_visible()
  • а
from pages.elements import Web_element
...
Web_element(main_page.top_menu_options_as_list).check_visibility_of_elements_in_the_list()

Тем самым мы говорим, что:

  • наш объект main_page.top_menu_options_as_list типа <class 'playwright.sync_api._generated.Locator'>
  • требуется преобразовать в экземпляр класса типа Web_element
  • и применить к нему метод этого класса для проверки списка элементов локатор на видимость => .check_visibility_of_elements_in_the_list()

Некорректное использование паттерна

Отсюда же вытекает большая проблема:

  • в тесте нам требуется импортировать класс from pages.elements import Web_element
    • это нужно, чтобы воспользоваться его методом .check_visibility_of_elements_in_the_list()
  • но возникает очевидный вопрос
    • зачем нужны дополнительные действия, если можно было изначально (то есть, в файле Main_page.py) объявить его типом Web_element.py и пользоваться этим методом напрямую.

Имеются и другие недочёты:

  • подразумевается, что экземпляр Main_page.py является страницей, то есть, должна быть возможность осуществлять над ним те же действия, что и со страницей, но на данный момент это не так. Всё, что сейчас делает этот класс - это хранит локаторы элементов.

На данный момент, мы не можем:

  • не закрыть страницу, воспользовавшись функцией main_page.close()
  • не перейти на другой адрес, воспользовавшись функцией main_page.goto("")